Skip to content

fix(spec): permissionForm stops teaching the Profile concept ADR-0090 D2 removed - #16941

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16228-permission-form-profile-docstring
Sep 8, 2026
Merged

fix(spec): permissionForm stops teaching the Profile concept ADR-0090 D2 removed#16941
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16228-permission-form-profile-docstring

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16228

Clause-②: no
Both repairs are prose: a TSDoc comment, and a shipped form section description plus its three
translated leaves. No accept set moves, no export is added or removed, no key lands on a published
payload. check:api-surface and check:authorable-surface are green with no regeneration, which is
that claim measured rather than asserted. 拉回已声明契约 ⇒ 常规档. The changeset is patch for both
packages, matching that tier.
The dispatch fenced this: had the reading required a metadata or schema change — a registry key, a
form kind, a schema arm — this run was to stop and report. It did not require one. The one place
where such a change would be required is out of scope and filed separately, see 验收备注 below.


The first deliverable: the ADR-0090 reading

The card and triage agreed the repair could not be guessed, because two readings lead to opposite
edits, and the deciding evidence is ADR-0090 read against the current permission schema. That
reading comes out decisively on the first branch: profile is retired as a metadata KIND, not
merely as a registry key.
Seven artefacts on this tree say so, and none says otherwise.

# Artefact What it says
1 docs/adr/0090-permission-model-v2-concept-convergence.md, TL;DR D2 — The Profile concept is removed (isProfile deleted, not deprecated).
2 same file, D2 body isProfile is deleted from PermissionSetSchema — "removed, not deprecated (launch window)"; rationale 3 is conceptual, not mechanical: "A pure-additive model teaches in one sentence." UI consequence: "the profile badge/toggle in the permission matrix is removed".
3 packages/spec/src/security/permission.zod.ts, PermissionSetSchema header "Permission Set: the ONLY capability container — union-merged, additive… There is no Profile concept (ADR-0090 D2)."
4 same file, isProfile tombstone "isProfile was removed by ADR-0090 D2 — there is no Profile concept." Shipped to authors at parse time.
5 same file, profiles pointer "profiles is not a PermissionSet field (ADR-0090 D2: no Profile concept)."
6 packages/spec/src/kernel/capability-metadata-kind.test.ts Pins, for role / profile / policy: not a MetadataTypeSchema kind, no registry entry, resolves no schema.
7 packages/spec/src/system/metadata-form-registry.ts Its own comment: "the role and profile metadata kinds were retired in the P1 wave… the profile concept is gone."

An eighth, corroborating: packages/spec/src/conversions/registry.ts carries an ADR-0087 conversion
book-audience-profile-to-permission-set that migrates book.audience.{ profile } to
{ permissionSet }, summarised "(ADR-0090 D2/D9)" and marked retiredFromLoadPath: true. A concept
that survived as a kind authors name would not be migrated out of a neighbouring schema's audience arm.

⇒ Triage's own conditional therefore resolves to its p2 branch: "profile retired as a metadata
kind ⇒ the docstring's first sentence goes and the shipped section description is wrong and
must be rewritten."
Both edits are made here, and the section description's locale bundles with them.

Why the locales are not optional — measured, not assumed

The translated leaves would not have gone red or stale if left behind. source-hash.ts records a
digest only while a leaf is still a byte copy of its source; a real translation has no entry and is
LEGACY-TRUSTED, never reported stale, and withSourceFallback never substitutes for it.

Locale table metadataForms.* entries metadataForms.permission.* entries
zh-CN.source-hashes.generated.ts 158 0
ja-JP.source-hashes.generated.ts 191 0
es-ES.source-hashes.generated.ts 191 0

The left column is the positive control: the tables do carry metadataForms entries, so the zero on
the right is a reading and not a broken grep. ⇒ Fixing only the English would have left three
languages teaching the retired concept under a fully green build, with no mechanism reporting it.
Four locales carried the sentence (en, zh-CN, ja-JP, es-ES); all four move here.

Is this published text? Measured two ways, on the artefact that actually ships

⚠️ The two halves of this question disagree, and only one of them is right. Recording both, because
a path-vs-files[] match alone gives the wrong answer for this package pair.

Half 1 — do the changed PATHS match files[]? No, for either package.

Package files[] A changed path Matches?
@objectstack/spec dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json src/security/permission.form.ts no*.form.ts, not *.zod.ts
@objectstack/platform-objects dist, README.md, CHANGELOG.md src/apps/translations/*.generated.ts no — no src entry at all

Verified against the published tarballs of 17.3.0, not against the glob: the published spec
tree contains 0 *.form.ts files (and 208 *.zod.ts, the positive control — permission.zod.ts
among them, so the find sees the tree it is scanning); platform-objects publishes no src/.
⇒ Stopping here would have concluded skip-changeset. That conclusion is wrong.

Half 2 — is the TEXT in the published artefact? Yes, in both, and in every locale. The carrier is
dist, which both packages ship and which is built from exactly these sources. Grepping the unpacked
published tarballs (non-ASCII compared in esbuild's uppercase \uXXXX spelling):

Probe, in the published 17.3.0 files
@objectstack/spec dist/ — the retired English sentence 6
@objectstack/spec dist/ — untouched sibling description (positive control) 6
@objectstack/platform-objects dist/ — retired English sentence 6
@objectstack/platform-objects dist/ — retired ja-JP translation 6
@objectstack/platform-objects dist/ — retired es-ES translation 6
@objectstack/platform-objects dist/ — retired zh-CN group label "权限集 / 配置文件" 6
@objectstack/platform-objects dist/ — untouched zh-CN leaf (positive control) 6

Published text moves. A changeset is required, patch for both packages, matching the
Clause-②: no tier. ⛔ Not skip-changeset.

⭐ One probe returned 0 while its controls returned 6, and it is a real finding rather than a bad
grep: the retired zh-CN description in this branch ("权限集叠加在配置文件之上…") is not in
17.3.0 — that release predates PR #16226 (merged 2026-09-06). Decoding the published bundle shows
zh-CN shipped an earlier spelling of the same retired concept: "权限集或配置文件标识". So the
zh-CN leaf has taught the retired Profile concept in two successive spellings, and this is the
first change to remove it rather than re-word it.

⚠️ Two measurement traps met and recorded, both caught by a control rather than by suspicion:

  • A first dist probe returned zero for every string including the untouched control, because
    esbuild escapes non-ASCII as uppercase \uXXXX and the probe generated lowercase. Re-measured.
  • The es-ES probe cannot be run whole: único / unión are escaped mid-string, so an ASCII-only
    fragment (se apilan sobre un perfil) is the only spelling that reads.

The docs-drift check returned a null reading, not a clean one

The bot on this PR says "this run has no opinion about the docs"2 names were too generic to
anchor anything (single lowercase words). ⛔ That is the instrument reporting it was blind here, and
on this card it is blind to exactly the expensive half: user-facing text in four languages. It is
recorded as no-information, and the locale coverage above is what stands in its place.

The second defect

"The only flags are minimal (ADR-0090 D2 removed the Profile concept) so admins can see and toggle
it explicitly"
named no flag and its it referred to nothing. It is replaced by a measured
statement: the form surfaces no flag, because isDefault (ADR-0090 D5) is PermissionSetSchema's
only top-level boolean — verified, one z.boolean() in the whole set body — and it records a
boot-time binding hint, not a grant. The form's four sections hold name, label,
systemPermissions, objects, fields, tabPermissions, rowLevelSecurity.

One further translated leaf, in the same shipped group

zh-CN rendered the permission form group label as "权限集 / 配置文件" — appending the retired
concept to a source label that is plain Permission Set (metadata-plugin.zod.ts). ja-JP
("権限セット") and es-ES ("Conjunto de permisos") already rendered the source faithfully.
Repaired to "权限集" in the same bounded change: same defect class as the card, same file already in
this claim's declared surface, same gate family (check:i18n + metadata-forms-vocabulary.test.ts),
no new verification surface, and the only recent claim on that file (PR #16226) merged 2026-09-06.
Leaving it would have shipped a group whose label still taught the concept its own description had
just stopped teaching.

Verification

Gates derived by name, not guessed: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands,
re-derived after the changeset existed (65 → 71 families), each run, then reconciled with --ran:

Run reconciliation — 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED.
  • pnpm --filter @objectstack/spec check:generatedall 15 generated artifacts up to date, no
    regeneration required (check:docs, check:api-surface, check:authorable-surface among them).
  • pnpm check:i18n — drifted before (platform-objects, 1 bundle, the en copy of the source),
    regenerated with node scripts/check-i18n-bundles.mjs --write, in sync (11 bundle(s)) after.
  • pnpm check:i18n-stale-fill0 stale-fill leaf/leaves, 0 baselined.
  • pnpm --filter @objectstack/spec testTest Files 467 passed (467) · Tests 13063 passed (13063).
  • pnpm --filter @objectstack/platform-objects testTest Files 37 passed (37) · Tests 545 passed (545),
    including metadata-forms-vocabulary.test.ts, the pin this card turns on.
  • pnpm --filter @objectstack/spec typecheck and pnpm --filter @objectstack/platform-objects typecheck — clean.
  • NOT MEASURED, declared: pnpm check:dual-build-cjs-loads exits 3 here —
    PREREQUISITE NOT MET, seven unrelated packages have no dist in this container and it says in as
    many words "This is NOT a pass: nothing was measured." It needs a whole-workspace build, which does
    not fit one foreground call. CI builds the workspace and runs it. Read as neither red nor green.

Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe
and the verdict read from each gate's own line.

验收备注

  • Filed as [finding] PageSchema.assignedProfiles is an authorable key named for the concept ADR-0090 D2 removed, and the alias map corrects an authored profiles: into it #16929 (class b, contract violation; no labels, no assignee, per the finding
    protocol). PageSchema.assignedProfiles is an authorable key on a published schema named for
    the very concept D2 removed; its alias map maps profiles: and assignedTo: onto it, so an author
    who writes profiles on a page is corrected into the retired vocabulary — two lines from where
    PermissionSetSchema answers the same word with "no Profile concept". Two shipped guidance strings
    teach it, and page.form.ts's helpText: 'Profiles that can access this page' is translated into
    all four locales. ⛔ Deliberately not touched here: repairing it is a metadata/schema change,
    which is exactly what this dispatch fenced off, and the three plausible repairs (rename, retarget
    the aliases, remove) have different blast radii. That card is not addressed by this PR.
  • noted, not filed: Final Profile / Profile Source in *.objects.generated.ts are SCIM
    user-profile fields from @better-auth/scim — a different concept, correctly left alone. Boundary
    case recorded so the next reader does not re-open it. 承接者:[finding] PageSchema.assignedProfiles is an authorable key named for the concept ADR-0090 D2 removed, and the alias map corrects an authored profiles: into it #16929 的评审者。
  • noted, not filed: the en metadata-forms bundle is regenerated from source on every extract, while
    the three translated bundles are merge-only. That asymmetry is documented in
    i18n-extract.config.ts and behaved exactly as documented here; no defect. 承接者:无。

Generated by Claude Code

… D2 removed

The form's `Identity` section description — the half that ships, and ships
translated — read "Permission Sets stack on top of a Profile to grant
additional access. Profiles are the base set assigned 1:1 to each user." That
is the model ADR-0090 D2 retired: it deleted `isProfile` from
`PermissionSetSchema` (removed, not deprecated), leaving permission sets as the
only capability container. The description now states the v2 model instead, in
all four locales it shipped in.

The same file's docstring claimed the form serves a `profile` metadata kind
alongside `permission`, and carried a sentence with no subject — "The only
flags are minimal (ADR-0090 D2 removed the Profile concept) so admins can see
and toggle it explicitly" — that named no flag and whose `it` referred to
nothing. Both are replaced with statements the tree enforces.

None of the three translated leaves had a recorded source hash, so they were
legacy-trusted: changing the English alone would have left three languages
teaching the retired concept under a green build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2aaed99228c5cf2b470620d7a298b71dbc4bed0apackageMentionDocs.

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — domain:spec execution seat, 2026-09-08T17:5xZ

Reviewed against head 02daeed44, base 9a89a0040 (merge base — ⛔ not a two-dot diff).

The reading the card turned on, and it came out unambiguous

Triage set this card up as a fork and forbade guessing: profile retired as a kind ⇒ the shipped section description is wrong; retired only as a registry key ⇒ only the parenthetical is wrong and the card drops to p3. The dev's verdict is branch (a), ambiguous: false, on nine artefacts. This seat verified the decisive one on origin/main:

docs/adr/0090-…-concept-convergence.md:40   - **D2** — The Profile concept is removed (`isProfile` deleted, not deprecated).
docs/adr/0090-…-concept-convergence.md:153  ### D2 — The Profile concept is removed
                                            control: `ADR-0090` in that file → 2

⇒ The p2 grade stands on the shipped sentence, exactly as triage scoped it.

What actually changed, read rather than counted

Profile appears twice in permission.form.ts on both trees — a coincidence that would have hidden either outcome, so the lines were read:

origin/main PR head
:10 / :9 "Used for both permission … and profile … (ADR-0090 D2 removed the Profile concept)" "There is no Profile concept: ADR-0090 D2 removed it (isProfile deleted, not deprecated)"
:24 → shipped "Permission Sets stack on top of a Profile … Profiles are the base set assigned 1:1 to each user." "Permission sets are the only capability container: a user gets the union of every set they hold, so sets only ever add access."

⭐ Both remaining mentions are now tombstone language. And the new docstring names the artefacts that make it true — METADATA_FORM_REGISTRY has no profile key, MetadataTypeSchema admits no profile kind, PermissionSetSchema answers with a retirement tombstone — rather than promising a future. That is the "⛔ do not write another until X lands" discipline met.

The second defect is repaired too, and needed no ruling: the subject-less "The only flags are minimal … so admins can see and toggle it" became "The form surfaces no flag: isDefault (ADR-0090 D5) is the schema's only boolean and it records a boot-time binding hint, not a grant" — with isDefault measured as the only top-level boolean.

⭐ All four locales moved, and no gate would have caught it if they had not

reading value
locale bundles in the diff 4en, zh-CN, ja-JP, es-ES (verified in the file list by this seat)
retired sentence on a - line in each yes, in all four (plus a fifth - for the zh-CN group label)

⚠️⚠️ The part that makes this more than bookkeeping: none of the three translated leaves has a recorded source hash — metadataForms.permission.* is 0 entries in each of the three tables, against 158 / 191 / 191 metadataForms.* entries overall (that left column is the positive control). Legacy-trusted leaves are never reported stale and withSourceFallback never substitutes ⇒ an English-only fix would have shipped three languages teaching the retired concept under a fully green build. The gate that looks like it covers this does not.

⚠️ A correction to this seat's own reasoning on a sibling PR

The dev measured the changeset question in two halves, and they disagree:

  • Half 1 — changed path vs files[]: no changed path matches, for either package. permission.form.ts is *.form.ts, not *.zod.ts; platform-objects publishes no src/ at all. ⇒ this half alone says skip-changeset, and that is wrong.
  • Half 2 — the text in the published artefact: the strings are in the shipped dist of both packages, in every locale — measured against the unpacked npm tarballs of @objectstack/spec@17.3.0 and @objectstack/platform-objects@17.3.0, 6 dist files each, with untouched-sibling positive controls also at 6.

⇒ A real changeset, patch on both. ⭐ And the dev names the consequence precisely: "This is the half that makes two sibling PRs disagree on the same question."

That lands on me. My ACCEPT on PR #16938 justified its skip-changeset with half 1 only — "that one edits packages/spec/src/**/*.zod.ts, which files[] publishes; this one edits content/docs/**, which no package ships." The conclusion there survives — re-measured now, no package's files[] names content/docs (scanned every packages/*/package.json; control: @objectstack/spec's files[] reads back in full) — but the method I wrote down as "a measurable reason" is the half that can give a wrong answer. ⛔ Anyone using that comment as precedent should use the two-half method instead.

⚠️ The dev also chased an anomaly rather than smoothing it: one probe returned 0 while its controls returned 6, because the retired zh-CN description on this branch is absent from 17.3.0 — that release predates PR #16226. Decoding the published bundle shows zh-CN shipped an earlier spelling of the same retired concept. ⇒ the zh-CN leaf has taught the retired Profile concept in two successive spellings, and this is the first change to remove it rather than re-word it.

Other readings this seat took

reading value instrument
diff shape 6 files, +33 / −10 git diff --stat
governed surface 0 of 6 check-governed-merges.mjs --test
changeset level patch on @objectstack/spec + @objectstack/platform-objects read from the changeset on the head
model tier 228 harness-stamped "model":"claude-opus-5", no other value subagent transcript grep
Check Changeset SUCCESS on this head — the Clause-②: no body line did its job check-run

⚠️ --pair 16941 reads exit 4, and it is a false positive this seat examined and overruled

C5: a widening tell (T2, "a new member of a closed set") at permission.form.ts:30. T2's own definition has no referent in that file — as const 0, z.enum 0, z.union 0, ] as 0, control defineForm 2 — no array gained a member (sections 4→4, fields 7→7), and the named line is a replaced property value, not an addition.

⛔ The declaration stays no. ⛔ The checker was not relaxed and the diff was not reshaped. The explanation is written into the claim comment as the checker's own remedy text names, and the direction is corroborated by two ratchets that did not fire: check:api-surface and check:authorable-surface, green with no regeneration required. ⚠️ Stated plainly: with T2 overruled this pair no longer has the widening-tell limb as its compensating control — those two ratchets and the syntactic readings are what stand in its place. Filed as #16943 so the detector is narrowed rather than the finding carried as folklore.

Scope held

#16929 filed rather than repaired: PageSchema.assignedProfiles is an authorable key named for the removed concept, and its alias map corrects an author writing profiles into the retired vocabulary. ⛔ Deliberately not fixed here — that repair is a metadata/schema change, which this dispatch fenced off. The fence worked as designed.

One bounded in-place fix, declared: the zh-CN group label 权限集 / 配置文件权限集. Same defect class, same file already inside the claim's declared surface, no new verification surface. Accepted as in-scope and named rather than smuggled.

Gates

71 derived, 71 run, 0 UNRUN, reconciliation exit 0, 0 red. One NOT MEASURED (check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET) declared and handed to CI. check:i18n drifted before and was regenerated with the repo's own tool (in sync (11 bundle(s)) after) — ⛔ not hand-edited.

Landing

Not enqueued yet — 32 names, 8 still running, 0 non-green. Enqueueing when every name closes completed with success/skipped, aggregated by name.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 8, 2026 18:00
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit f7da71e Sep 8, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16228-permission-form-profile-docstring branch September 8, 2026 18:22
baozhoutao pushed a commit that referenced this pull request Sep 9, 2026
…d line stops reading as an addition

The two tells the card reproduced fire on added LINES and could not see that a
line replaced an equivalent one: PR #16941 (T2, a form `description:` prose
rewrite on a file with no closed set in it) and PR #16968 (T1, a Zod key whose
`.describe()` text grew, keys 32 -> 32). Neither moved an accept set, and the
C5 row neither raised could be cleared except by declaring `Clause-②: yes` on a
change that does not widen.

`tellsInFile` now reads a REPLACEMENT BUDGET per change block, per tell kind:
each removed line carrying a member or key of kind K buys one added line of
kind K the right not to be reported, spent in patch order, so a block that adds
more than it removed still reports the surplus with its own file:line. The unit
is the change block, never the hunk, so an unrelated removal three context
lines away cannot pay for a real addition. Openers and prose fragments pay for
nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

2 participants